#! /usr/bin/env python
# 
# Please use the following style guide if you are updating this code:
#
# http://www.python.org/dev/peps/pep-0008/
#
import logging
import optparse
import re
import os.path
from datetime import datetime, timedelta, tzinfo
from getpass import getpass
from time import sleep
import opentick

class DataRequest:
    """Class used to specify various attributes of a data request 
    to SymbolHistClient.

    """
    def __init__(self):
        self.ot_server = "feed1.opentick.com"
        self.ot_server_port = 10010
        self.account_login = None
        self.account_password = None
        self.exchange_code = None
        self.symbol_code = None
        #
        # from_date and to_date are datetime objects that specify the
        # duration for which historical data is to be retrieved. Both
        # times are expected to be in UTC.
        #
        self.from_date = None
        self.to_date = None
        #
        # If True then display verbose messages during execution.
        #
        self.is_verbose = False
        #
        # A file object that will be used for writing received
        # historical data.
        #
        self.output_file = None
        self.column_separtor = ' '
        return 

class SymbolHistClient( opentick.OTClient ):
    """Request historical data for a specified symbol and time period from
    the OTFeed servers. This class obtains the requested historical
    data and saves it to a user-specified file. The data request is
    provided to the GetHistData() method as an instance of the
    DataRequest class.
    """

    class InvalidDataRequestError( Exception ):
        """Exception thrown when an invalid data request is encountered by the
        GetHistData() method of this class.
        
        """
        def __init__( self, errorMesssage ):
            self.error_message = errorMesssage 
            return

    def __init__( self ):
        """Constructor. Initialize the state of a new class instance."""
        super( SymbolHistClient, self ).__init__()
        #
        # Initialize the state of this instance.
        #
        self.__is_login_successful = False 
        self.__is_hist_data_received = False
        self.__is_error_encountered = False
        #
        self.__data_request = None
        self.__data_request_id  = None
        #
        return

    def GetHistData( self, dataRequest ):
        """Request historical data based on contents of dataRequest. 

        """
        #
        # Save the current data request so other methods of the class
        # can access it if needed.
        #
        self.__CheckDataRequest( dataRequest )
        self.__data_request = dataRequest 
        #
        # If the output file exists then refuse to overwrite it.
        # :TODO: Add an option to force overwriting existing files.
        #
        if os.path.exists( self.__data_request.output_file ):
            raise self.InvalidDataRequestError( "File " + self.__data_request.output_file +
                                                " exists. Will not overwrite." )
        #
        # Open the specified file for writing.
        #
        self.__output_file = open( self.__data_request.output_file,
                                   "w" )
        #
        # Convert the user-specified times into the format expected by
        # OpenTick library. See:
        # http://www.opentick.com/dokuwiki/doku.php?id=general:date_time_issues
        #
        # Get the time difference between from_date and to_date and epoch.
        #
        self.__epoch_utc = datetime( 1970, 1, 1 ) 
        self.__seconds_per_day = 3600 * 24
        start_time_epoch_delta = self.__data_request.from_date - self.__epoch_utc
        stop_time_epoch_delta = self.__data_request.to_date - self.__epoch_utc
        #
        # Convert the two time differences into number seconds. 
        #
        start_time = ( self.__seconds_per_day * start_time_epoch_delta.days + 
                       start_time_epoch_delta.seconds )
        stop_time = ( self.__seconds_per_day * stop_time_epoch_delta.days + 
                      stop_time_epoch_delta.seconds )
        #
        # Use the specified opentick server and port to get the data.
        #
        self.addHost( self.__data_request.ot_server,
                      self.__data_request.ot_server_port )
        #
        # Log on to the server. 
        #
        logging.info( "Sending login request for user " + 
                      self.__data_request.account_login )
        self.login( self.__data_request.account_login,
                    self.__data_request.account_password )
        #
        # Wait for the login to complete.
        #
        while ( ( not self.__is_error_encountered ) and 
                ( not self.__is_login_successful ) ):
            sleep(1)
        #
        if self.__is_error_encountered:
            raise
        #
        # Request historical data.
        #
        logging.info( "Requesting symbol history." )
        data_entity = opentick.OTDataEntity( self.__data_request.exchange_code,
                                             self.__data_request.symbol_code )

        self.__data_request_id = self.requestHistData( data_entity,
                                                       start_time,
                                                       stop_time,
                                                       #opentick.OT_HIST_RAW_TICKS ) 
                                                       opentick.OT_HIST_OHLC_MINUTELY )
        #
        # Wait for the historical data to be received. 
        #
        while( ( not self.__is_error_encountered ) and
               ( not self.__is_hist_data_received ) ):
            sleep(1)
        #
        if self.__is_error_encountered:
            raise
        #
        return

    def onMessage( self, otMessage ):
        #
        OT_LIST_SYMBOLS = 10 
        if ( ( otMessage.getRequestId() == self.__data_request_id ) and
             ( OT_LIST_SYMBOLS == otMessage.getCode() ) ):
            #
            logging.info( "Symbol history data received successfully." )
            self.__is_hist_data_received = True
        #
        return

    def onHistQuote( self, otQuote ):
        #
        # Convert the timestamp into a datetime instance.
        #
        days_since_epoch = ( otQuote.getTimestamp() / self.__seconds_per_day )
        remainder_secs = ( otQuote.getTimestamp() % self.__seconds_per_day )
        time_stamp = self.__epoch_utc + timedelta( days_since_epoch, remainder_secs )
        #
        # Write the data to the specified output file.
        #
        output_data = [ otQuote.getTimestamp(), \
                        time_stamp, \
                        otQuote.getAskSize(), \
                        otQuote.getBidSize(), \
                        otQuote.getIndicator(), \
                        otQuote.getTickIndicator(), \
                        otQuote.getAskPrice(),\
                        otQuote.getBidPrice() ]
        for i in output_data:
            if i != output_data[0]:
                self.__output_file.write( self.__data_request.column_separtor )
            self.__output_file.write( str(i) )
        #
        self.__output_file.write( '\n' )
        #
        return

    def onHistOHLC( self, otOhlcData ):
        #
        # Convert the timestamp into a datetime instance.
        #
        days_since_epoch = ( otOhlcData.getTimestamp() / self.__seconds_per_day )
        remainder_secs = ( otOhlcData.getTimestamp() % self.__seconds_per_day )
        time_stamp = self.__epoch_utc + timedelta( days_since_epoch, remainder_secs )
        #
        # Write the data to the specified output file.
        #
        output_data = [ otOhlcData.getTimestamp(), \
                        time_stamp, \
                        otOhlcData.getOpenPrice(), \
                        otOhlcData.getHighPrice(), \
                        otOhlcData.getLowPrice(), \
                        otOhlcData.getClosePrice(), \
                        otOhlcData.getVolume() ]
        for i in output_data:
            if i != output_data[0]:
                self.__output_file.write( self.__data_request.column_separtor )
            self.__output_file.write( str(i) )
        #
        self.__output_file.write( '\n' )
        #
        return

    def onLogin( self ):
        logging.info( "Login successful." )
        #
        self.__is_login_successful = True
        return 

    def onError( self, otError ):
        logging.error( "[" + str(otError.getRequestId()) + ":" + 
                       str(otError.getCode() ) +
                       "] " + otError.getDescription() )
        self.__is_error_encountered = True
        return     

    def __CheckDataRequest( self, dataRequest ):
        """Make sure that contents of dataRequest are valid and throw an
        appropriate exception if they are not.

        """
        if dataRequest is None:
            raise self.InvalidDataRequestError( "Invalid data request." )

        if not isinstance( dataRequest, DataRequest ):
            raise self.InvalidDataRequestError( 
                "Data request must be an instance of class DataRequest." )

        if dataRequest.ot_server is None:
            raise self.InvalidDataRequestError( "No OTFeed server specified." )

        if dataRequest.from_date is None:
            raise self.InvalidDataRequestError( "No start time specified." )

        if dataRequest.to_date is None:
            raise self.InvalidDataRequestError( "No stop time specified." )

        if dataRequest.to_date < dataRequest.from_date:
            raise self.InvalidDataRequestError( "Start date must be before end date." )

        if dataRequest.exchange_code is None:
            raise self.InvalidDataRequestError( "No exchange code specified." )

        if dataRequest.symbol_code is None:
            raise self.InvalidDataRequestError( "No symbol code specified." )

        if dataRequest.output_file is None:
            raise self.InvalidDataRequestError( "No output file specified." )

        return
# -----------------------------------------------------------------
def create_options_parser():
    """Create a parser to extract value from command line options."""
    parser = optparse.OptionParser( )
    parser.add_option( "-u", "--user",
                       action = "store",
                       type = "string",
                       help = "OpenTick account/user name" )
    parser.add_option( "-p", "--password",
                       action = "store",
                       type = "string",
                       help = "OpenTick account/user password" )
    parser.add_option( "-e", "--exchange",
                       action = "store",
                       type = "string",
                       help = "Exchange code for which symbol list is to be obtained" )
    parser.add_option( "-s", "--symbol",
                       action = "store",
                       type = "string",
                       help = "Symbol code for which data is to be obtained" )
    parser.add_option( "-o", "--outfile",
                       action = "store",
                       type = "string",
                       help = "Output file name" )
    parser.add_option( "-H", "--server",
                       action = "store",
                       type = "string",
                       help = "OpenTick server host name" )
    parser.add_option( "-P", "--port",
                       action = "store",
                       type = "int",
                       help = "OpenTick server port" )
    parser.add_option( "-F", "--from_date",
                       action = "store",
                       type = "string",
                       help = "Start date for historical data" )
    parser.add_option( "-T", "--to_date",
                       action = "store",
                       type = "string",
                       help = "End date for historical data" )
    parser.add_option( "-S", "--separator",
                       action = "store",
                       type = "string",
                       help = "Column separator in output file" )
    return parser
# -----------------------------------------------------------------
# A class building tzinfo objects for fixed-offset time zones.
# Note that FixedOffset(0, "UTC") is a different way to build a
# UTC tzinfo object.
#
# See: http://docs.python.org/lib/datetime-tzinfo.html
class FixedOffset(tzinfo):
    """Fixed offset in minutes east from UTC."""

    def __init__(self, offset, name):
        self.__offset = timedelta(minutes = offset)
        self.__name = name

    def utcoffset(self, dt):
        return self.__offset

    def tzname(self, dt):
        return self.__name

    def dst(self, dt):
        return ZERO

# -----------------------------------------------------------------
def parse_date_time(s):
    """Create datetime object representing date/time expressed in a
    string.
    
    Takes a string in the format produced by calling str() on a python
    datetime object and returns a datetime instance that would produce
    that string.

    Acceptable formats are: "YYYY-MM-DD HH:MM:SS.ssssss+HH:MM",
                            "YYYY-MM-DD HH:MM:SS.ssssss",
                            "YYYY-MM-DD HH:MM:SS+HH:MM",
                            "YYYY-MM-DD HH:MM:SS"

    Where ssssss represents fractional seconds. The timezone is
    optional and may be either positive or negative hours/minutes east
    of UTC.

    See
    http://kbyanc.blogspot.com/2007/09/python-reconstructing-datetimes-from.html
    """
    if s is None:
        return None
    #
    # Split string in the form 2007-06-18 19:39:25.3300-07:00 into its
    # constituent date/time, microseconds, and timezone fields where
    # microseconds and timezone are optional.
    #
    m = re.match(r'(.*?)(?:\.(\d+))?(([-+]\d{1,2}):(\d{2}))?$',
                 str(s))
    datestr, fractional, tzname, tzhour, tzmin = m.groups()
    #
    # Create tzinfo object representing the timezone expressed in the
    # input string.  The names we give for the timezones are lame:
    # they are just the offset from UTC (as it appeared in the input
    # string).  We handle UTC specially since it is a very common case
    # and we know its name.
    #
    if tzname is None:
        tz = None
    else:
        tzhour, tzmin = int(tzhour), int(tzmin)
        if tzhour == tzmin == 0:
            tzname = 'UTC'
        tz = FixedOffset(timedelta(hours=tzhour,
                                   minutes=tzmin), tzname)
    #
    # Convert the date/time field into a python datetime
    # object.
    #
    x = datetime.strptime(datestr, "%Y-%m-%d %H:%M:%S")
    #
    # Convert the fractional second portion into a count
    # of microseconds.
    #
    if fractional is None:
        fractional = '0'
    #
    fracpower = 6 - len(fractional)
    fractional = float(fractional) * (10 ** fracpower)
    #
    # Return updated datetime object with microseconds and
    # timezone information.
    #
    return x.replace(microsecond=int(fractional), tzinfo=tz)
# -----------------------------------------------------------------
def create_datarequest_from_options( userOptions ):
    #
    data_request = DataRequest()
    if not userOptions.user is None:
        data_request.account_login = userOptions.user

    if not userOptions.password is None: 
        data_request.account_password = userOptions.password

    if not userOptions.exchange is None:
        data_request.exchange_code = userOptions.exchange

    if not userOptions.symbol is None:
        data_request.symbol_code = userOptions.symbol

    if not userOptions.outfile is None:
        data_request.output_file = userOptions.outfile

    if not userOptions.server is None:
        data_request.ot_server = userOptions.server

    if not userOptions.port is None:
        data_request.ot_server_port = userOptions.port

    if not userOptions.from_date is None:
        data_request.from_date = parse_date_time( userOptions.from_date )

    if not userOptions.to_date is None:
        data_request.to_date = parse_date_time( userOptions.to_date )

    if not userOptions.separator is None:
        data_request.column_separtor = userOptions.separator
    return data_request
# -----------------------------------------------------------------
def main():
    #
    # Parse the command line options specified by the user.
    #
    parser = create_options_parser()
    (options, arguments) = parser.parse_args()
    #
    # Create an instance of our OpenTick client and add a server host.
    #
    client = SymbolHistClient()
    #
    # Fill in a data request instance with some default values. 
    # In the future these will be command line options to the script.
    #
    data_request = create_datarequest_from_options( options )
    #
    # Ask the user for a login account name and password to connect to the 
    # OpenTick server if they have not been provided on the command line
    # already.
    #
    if not data_request.account_login:
        data_request.account_login = raw_input( "Enter the user name: " )
    if not data_request.account_password:
        data_request.account_password = getpass( "Password for " + 
                                                 data_request.account_login + 
                                                 ": " )
    #
    client.GetHistData( data_request )
    #
    return
###############################################################################
# The script starts here.
###############################################################################
if __name__ == "__main__":
    try:
        logging.basicConfig( level  = logging.DEBUG,
                             format = '%(asctime)s %(levelname)s %(message)s' )
        main()
    except SystemExit:
        raise
    except:
        print \
"""An internal error occurred. Please report all the output from the
program, including the following traceback to crayzeewulf@gmail.com.
"""
        raise
    
